Skip to content

support parallel snapshotting for uuid partitioning#4482

Merged
jgao54 merged 2 commits into
mainfrom
string-partitioning
Jun 25, 2026
Merged

support parallel snapshotting for uuid partitioning#4482
jgao54 merged 2 commits into
mainfrom
string-partitioning

Conversation

@jgao54

@jgao54 jgao54 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Add parallel snapshotting support for MySQL when default primary key is a UUID string column.

We best-effort detect uuid by evaluating the min and max values in the table; if both min and max value match uuid format (and have a consistent hex casing), we can guarantee correctness (see description for DetectUuidWithHexCasing for a bit more details). Original plan was to sample first and last N records, but it doesn't add a ton of value here, so just checking min/max values for simplicity.

If string column is deemed non-UUID, we fallback to full table partition for now; will follow-up with partitioning for arbitrary string.

Testing: add unit and integration tests to ensure uuids PKs are parallel snapshotted while arbitrary string PKs still correctly replicate with full table partition.

Fixes: DBI-868

@jgao54 jgao54 force-pushed the string-partitioning branch 3 times, most recently from e7d0d7c to 9b26c4a Compare June 24, 2026 04:05
@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
2358 3 2355 220
View the top 3 failed test(s) by shortest run time
github.com/PeerDB-io/peerdb/flow/e2e::TestPeerFlowE2ETestSuiteMySQL_CH_Cluster
Stack Traces | 0.01s run time
=== RUN   TestPeerFlowE2ETestSuiteMySQL_CH_Cluster
=== PAUSE TestPeerFlowE2ETestSuiteMySQL_CH_Cluster
=== CONT  TestPeerFlowE2ETestSuiteMySQL_CH_Cluster
--- FAIL: TestPeerFlowE2ETestSuiteMySQL_CH_Cluster (0.01s)
github.com/PeerDB-io/peerdb/flow/e2e::TestPeerFlowE2ETestSuiteMySQL_CH_Cluster/Test_MySQL_String_Partition_Key_Arbitrary_FullTable
Stack Traces | 61.3s run time
=== RUN   TestPeerFlowE2ETestSuiteMySQL_CH_Cluster/Test_MySQL_String_Partition_Key_Arbitrary_FullTable
=== PAUSE TestPeerFlowE2ETestSuiteMySQL_CH_Cluster/Test_MySQL_String_Partition_Key_Arbitrary_FullTable
=== CONT  TestPeerFlowE2ETestSuiteMySQL_CH_Cluster/Test_MySQL_String_Partition_Key_Arbitrary_FullTable
2026/06/24 05:55:43 INFO Received AWS credentials from peer for connector: ci x-peerdb-additional-metadata={Operation:FLOW_OPERATION_UNKNOWN}
2026/06/24 05:55:43 INFO Received AWS credentials from peer for connector: clickhouse x-peerdb-additional-metadata={Operation:FLOW_OPERATION_UNKNOWN}
2026/06/24 05:55:44 INFO fetched schema x-peerdb-additional-metadata={Operation:FLOW_OPERATION_UNKNOWN} table=e2e_test_mychcl_omj0shcw.test_date_coercion
    clickhouse_mysql_test.go:1748: UNEXPECTED STATUS TIMEOUT STATUS_SETUP
--- FAIL: TestPeerFlowE2ETestSuiteMySQL_CH_Cluster/Test_MySQL_String_Partition_Key_Arbitrary_FullTable (61.26s)
github.com/PeerDB-io/peerdb/flow/e2e::TestPeerFlowE2ETestSuiteMySQL_CH_Cluster/Test_MySQL_String_Partition_Key_UUID_Parallel_Snapshot
Stack Traces | 61.3s run time
=== RUN   TestPeerFlowE2ETestSuiteMySQL_CH_Cluster/Test_MySQL_String_Partition_Key_UUID_Parallel_Snapshot
=== PAUSE TestPeerFlowE2ETestSuiteMySQL_CH_Cluster/Test_MySQL_String_Partition_Key_UUID_Parallel_Snapshot
=== CONT  TestPeerFlowE2ETestSuiteMySQL_CH_Cluster/Test_MySQL_String_Partition_Key_UUID_Parallel_Snapshot
2026/06/24 05:56:03 INFO Received AWS credentials from peer for connector: ci x-peerdb-additional-metadata={Operation:FLOW_OPERATION_UNKNOWN}
2026/06/24 05:56:03 INFO fetched schema x-peerdb-additional-metadata={Operation:FLOW_OPERATION_UNKNOWN} table=e2e_test_mychcl_0at45llj.test_extra_ch_cols
    clickhouse_mysql_test.go:1687: UNEXPECTED STATUS TIMEOUT STATUS_SETUP
--- FAIL: TestPeerFlowE2ETestSuiteMySQL_CH_Cluster/Test_MySQL_String_Partition_Key_UUID_Parallel_Snapshot (61.29s)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@github-actions

Copy link
Copy Markdown
Contributor

❌ Test Failure

Analysis: The failures are a real bug in the PR's new partitioning logic — Test_Partition_Key_Empty fails with a deterministic, reproducible assertion ("expected no partitions to be created, but 1 was") on both CH and CH_Cluster variants, alongside the PR's own new partition-snapshot tests timing out in setup.
Confidence: 0.88

⚠️ This appears to be a real bug - manual intervention needed

View workflow run

@github-actions

Copy link
Copy Markdown
Contributor

❌ Test Failure

Analysis: Deterministic partition-key test failures (e.g. "expected no partitions to be created, but was 1") reproduce identically across all three MySQL matrix jobs and hit exactly the partition-key feature this "string-partitioning" PR modifies, indicating a real bug rather than flakiness.
Confidence: 0.92

⚠️ This appears to be a real bug - manual intervention needed

View workflow run

@jgao54 jgao54 force-pushed the string-partitioning branch from 9b26c4a to af27673 Compare June 24, 2026 05:39
@github-actions

Copy link
Copy Markdown
Contributor

❌ Test Failure

Analysis: The PR's own new feature tests (Test_MySQL_String_Partition_Key_*) deterministically fail with "UNEXPECTED STATUS TIMEOUT STATUS_SETUP" identically across all three MySQL flavors, indicating the branch's new string/UUID partitioning setup logic is stuck in mirror setup — a real bug, not a flaky failure.
Confidence: 0.92

⚠️ This appears to be a real bug - manual intervention needed

View workflow run

@jgao54 jgao54 force-pushed the string-partitioning branch from af27673 to acc33e8 Compare June 24, 2026 06:17
@jgao54 jgao54 force-pushed the string-partitioning branch from acc33e8 to dd731c6 Compare June 24, 2026 07:54
@jgao54 jgao54 marked this pull request as ready for review June 24, 2026 08:02
@jgao54 jgao54 requested a review from a team as a code owner June 24, 2026 08:02
@claude

claude Bot commented Jun 24, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Comment thread flow/connectors/utils/partition.go Outdated
)

// AddUuidStringPartitionsWithRange splits a UUID watermark range into partitions.
func (p *PartitionHelper) AddUuidStringPartitionsWithRange(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to scope the PR to MySQL only so intentionally using a separate method here that gets explicitly called by mysql connector.

AddPartitionsWithRange is used by Postgres as well so enabling it for PG (e.g. when a custom string column is selected as the watermark column) requires more code changes and e2e testing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to move this method to an utils package within the MySQL connector subtree to prevent accidental usage? Or at least add a comment? I can imagine a happy agent or engineer thinking oh, this is convenient

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, can move it to mysql for now and bring it back when we are ready to make it generic.

Comment thread flow/connectors/utils/partition.go Outdated
)

// AddUuidStringPartitionsWithRange splits a UUID watermark range into partitions.
func (p *PartitionHelper) AddUuidStringPartitionsWithRange(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to move this method to an utils package within the MySQL connector subtree to prevent accidental usage? Or at least add a comment? I can imagine a happy agent or engineer thinking oh, this is convenient

Comment thread flow/connectors/utils/partition_test.go Outdated
maxV string
expectedCase *regexp.Regexp
}{
{"lower", "018f6e7a-1b2c-7def-8a3b-1c2d3e4f5a6b", "f47ac10b-58cc-4372-a567-0e02b2c3d479", UuidLowerRe},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make sense to add cases for the non-happy path cases just to confirm nothing crash. e.g:

lower:"not an uuid" upper: "018f6e7a-1b2c-7def-8a3b-1c2d3e4f5a6b"
lower: "f47ac10b-58cc-4372-a567-0e02b2c3d479" upper:"018f6e7a-1b2c-7def-8a3b-1c2d3e4f5a6b"
...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, had a separate test for this, but looks like i accidentally deleted them when addressing a merge conflict.

@jgao54 jgao54 Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added non-happy path testing for buildUuidStringPartitions:


func TestBuildUuidStringPartitionsInvalid(t *testing.T) {
	cases := []struct {
		name string
		minV string
		maxV string
	}{
		{"arbitrary string", "apple", "banana"},
		{"inverted", "f47ac10b-58cc-4372-a567-0e02b2c3d479", "018f6e7a-1b2c-7def-8a3b-1c2d3e4f5a6b"},
		{"too short", "018f6e7a-1b2c", "f47ac10b-58cc"},
		{"too long", "018f6e7a-1b2c-7def-8a3b-1c2d3e4f5a6bb", "f47ac10b-58cc-4372-a567-0e02b2c3d4799"},
	}
	const numPartitions = 32
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			_, err := buildUuidStringPartitions(tc.minV, tc.maxV, hexCasingLower, numPartitions)
			require.Error(t, err)
		})
	}
}

Failure here is intentional since it indicates a code error on our end if buildUuidStringPartitions ends up processing invalid uuids. They should be caught by detectUuidWithHexCasing, which also test for invalid cases in TestDetectUUIDCase. There's also an e2e test Test_MySQL_String_Partition_Key_Arbitrary_FullTable to make sure that we default to full table partition. So we should be good on coverage now.

Comment thread flow/connectors/utils/partition.go Outdated
// Because only the bounds are examined, the classification can be wrong in
// the following rare cases:
// - non-UUID rows can exist between UUID-shaped min/max bounds;
// - non-boundary UUIDs contain a mix of upper and lower case

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not possible to normalize the string values both at this partition computation level and at query level so at least for parallel snapshotting we are case insensitive?

@jgao54 jgao54 Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can maintain correctness as long as the min/max values (in the first and last partitions) use the original casing (which is the case today). However, if table collation is case-sensitive, it can lead to data skew.

Skew example: say we have min/max uuids A0000..... and e0000...; let's say we normalize the computed boundaries to lower case and got the following bounds:

  • P1 [A0000…, accccc…)
  • P2 [accccc…, b99999…)
  • P3 [b99999…, c66666…)
  • P4 [c66666…, d33333…)
  • P5 [d33333…, e0000…]

This means P1 would cover all the upper case uuids, leading to potential skew if table was recently switched to lower case.

Another skew example where we have mixed casing within an UUID: say we have min/max uuid A000a... and E000e...; and again we normalize the computed boundaries to lower case and got the following bounds

  • P1 [A000a…, accccc…)
  • P2 [accccc…, b99999…)
  • P3 [b99999…, c66666…)
  • P4 [c66666…, d33333…)
  • P5 [d33333…, E000e…]

In this case all data ends up in the first partition, and the other computed partition boundaries return empty results.

We could get a bit smarter with what default casing to use, but it just adds more code complexity. Hence I was thinking we can treat mix-case as arbitrary string for more even partitioning.

@jgao54 jgao54 Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, perhaps by normalize at computation/query level you probably mean something like where lower(<uuid_col>) >= <start_value_lower_case> and lower(<uuid_col>) < <end_value_lower_case>. This would work but cannot be done cheaply since it can't take advantage of the index, so we end up with full table scans.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, perhaps by normalize at computation/query level you probably mean something like where lower(<uuid_col>) >= <start_value_lower_case> and lower(<uuid_col>) < <end_value_lower_case>.

Yes, that's what I meant actually.

since it can't take advantage of the index, so we end up with full table scans.

Great point! I hadn't considered the index!

@pfcoperez pfcoperez left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@jgao54 jgao54 merged commit ff4139e into main Jun 25, 2026
30 checks passed
@jgao54 jgao54 deleted the string-partitioning branch June 25, 2026 07:06
andreyzhelnin-st added a commit to andreyzhelnin-st/peerdb that referenced this pull request Jun 25, 2026
- Add end_inclusive to StringPartitionRange proto (aligns with MySQL PR
  PeerDB-io#4482 adding the same field; avoids future merge conflict)
- Replace sentinel-string approach with end_inclusive=true on the last
  string partition, using $lte instead of $lt for its MongoDB filter
- Sort string _id samples via MongoDB $sort stage instead of Go sort,
  preserving the collection's collation order
- Filter boundary samples by exact equality instead of <=/>= comparison,
  trusting MongoDB's min/max guarantees
- Add e2e tests for string and numeric _id partitioned snapshot flows
- Document the double _id gap limitation on numericPartitions (follow-up)
andreyzhelnin-st added a commit to andreyzhelnin-st/peerdb that referenced this pull request Jun 26, 2026
- Add end_inclusive to StringPartitionRange proto (aligns with MySQL PR
  PeerDB-io#4482 adding the same field; avoids future merge conflict)
- Replace sentinel-string approach with end_inclusive=true on the last
  string partition, using $lte instead of $lt for its MongoDB filter
- Sort string _id samples via MongoDB $sort stage instead of Go sort,
  preserving the collection's collation order
- Filter boundary samples by exact equality instead of <=/>= comparison,
  trusting MongoDB's min/max guarantees
- Add e2e tests for string and numeric _id partitioned snapshot flows
- Document the double _id gap limitation on numericPartitions (follow-up)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants